package com.ar4j.spring;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.apache.commons.beanutils.PropertyUtils;

import com.ar4j.ActiveRecord;
import com.ar4j.ActiveRecordJsonWrapper;
import com.ar4j.IActiveRecordFactory;
import com.ar4j.type.ITypeConverter;

/**
 * A factory that is able to produce new blank active record instances as well as a shared
 * instance with limited access to read-only methods.
 */
public class SpringActiveRecordAdapter<T extends ActiveRecord<T>> {
  private static final Set<String> DEFAULT_DISALLOWED_METHODS = new HashSet<String>(Arrays.asList(new String[] {
      "getProperty",
      "setProperty",
      "getPropertyMap",
      "writeReplace",
      "wrapInJson",
      "equals",
      "isSameContent",
      "toString",
      "hashCode",
      "cloneRecord",
      "getIdentifier",
      "isNewRecord",
      "reload"
    }));
  
  private Class<T> baseClass;
  private IActiveRecordFactory factory;
  private T readOnlyInstance;
    
  public SpringActiveRecordAdapter(Class<T> baseClass, IActiveRecordFactory factory) {
    this.baseClass = baseClass;
    this.factory = factory;
    initializeReadOnlyInstance();
  }
  
  /**
   * @return the factory behind this adapter
   */
  public IActiveRecordFactory getFactory() {
    return factory;
  }
    
  /**
   * @return a fresh blank record of the generic type of this adapter
   */
  public T blank() {
    return factory.getActiveRecord(baseClass);
  }
  
  /**
   * @return a read-only active record of the generic type of this adapter. Can be used to run lookup operations.
   */
  public T readOnly() {
    return readOnlyInstance;
  }
  
  /**
   * @return a newly created instance from the given wrapper, synonym for {@link com.ar4j.IActiveRecordFactory#getActiveRecord(Class, ActiveRecordJsonWrapper)}
   */
  public T fromWrapper(ActiveRecordJsonWrapper wrapper) {
    return factory.getActiveRecord(baseClass, wrapper);
  }
  
  /**
   * @return a newly created instance from the given prototype, synonym for {@link com.ar4j.IActiveRecordFactory#getActiveRecord(ActiveRecord)}
   */
  public T fromPrototype(T prototype) {
    return factory.getActiveRecord(prototype);
  }
  
  /**
   * @return a newly created instance from the given map, synonym for {@link com.ar4j.IActiveRecordFactory#getActiveRecord(Class, Map)}
   */
  public T fromMap(Map<String, Object> values) {
    return factory.getActiveRecord(baseClass, values);
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getFactoryName()
   */
  public String getFactoryName() {
    return readOnlyInstance.getFactoryName();
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getBaseClass()
   */
  public Class<T> getBaseClass() {
    return readOnlyInstance.getBaseClass();
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getPropertyNames()
   */
  public Set<String> getPropertyNames() {
    return readOnlyInstance.getPropertyNames();
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getPropertyType(String)
   */
  public Class<?> getPropertyType(String name) {
    return readOnlyInstance.getPropertyType(name);
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#hasProperty(String)
   */
  public boolean hasProperty(String name) {
    return readOnlyInstance.hasProperty(name);
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#isPropertyBackingStoreReadable(String)
   */
  public boolean isPropertyBackingStoreReadable(String name) {
    return readOnlyInstance.isPropertyBackingStoreReadable(name);
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#isPropertyBackingStoreWritable(String)
   */
  public boolean isPropertyBackingStoreWritable(String name) {
    return readOnlyInstance.isPropertyBackingStoreWritable(name);
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getIdentifierPropertyName()
   */
  public String getIdentifierPropertyName() {
    return readOnlyInstance.getIdentifierPropertyName();
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getIdentifierPropertyType()
   */
  public Class<?> getIdentifierPropertyType() {
    return readOnlyInstance.getIdentifierPropertyType();
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getTableName()
   */
  public String getTableName() {
    return readOnlyInstance.getTableName();
  }
  
  /**
   * Delegate method for this adapter's base class's informational methods. Executed via the readOnly instance.
   * @see com.ar4j.ActiveRecord#getPropertyTypeConverter(String)
   */
  public ITypeConverter<?, ?> getPropertyTypeConverter(String name) {
    return readOnlyInstance.getPropertyTypeConverter(name);
  }
  
  /**
   * Scans the base class for the name of property getter/setter methods and
   * creates a filtering proxy disallowing access to those methods as well as
   * the default disallowed methods on a blank instance of the base class (from
   * the given factory)
   */
  @SuppressWarnings("unchecked")
  private void initializeReadOnlyInstance() {
    try {
      // get a base instance from the factory
      T obj = factory.getActiveRecord(baseClass);
      
      // gather a set of disallowed fields (default + get/set)
      Set<String> disallowed = new HashSet<String>(DEFAULT_DISALLOWED_METHODS);
      for(String property : obj.getPropertyNames()) {
        PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(obj, property);
        Method readMethod = pd.getReadMethod();
        if(readMethod != null) {
          disallowed.add(readMethod.getName());
        }
        
        Method writeMethod = pd.getWriteMethod();
        if(writeMethod != null) {
          disallowed.add(writeMethod.getName());
        }
      }
      
      // create the filtering proxy
      Enhancer enhancer = new Enhancer();
      enhancer.setUseFactory(true);
      enhancer.setUseCache(true);
      enhancer.setClassLoader(baseClass.getClassLoader());
      enhancer.setSuperclass(baseClass);
      enhancer.setCallback(new ActiveRecordFilteringMethodInterceptor(obj, disallowed));
      
      // create the proxy
      this.readOnlyInstance = (T) enhancer.create();
      
    } catch(Exception e) {
      throw new RuntimeException("Could not create a filtered instance for: " + baseClass, e);
    }
  }
  
  /**
   * A method intercepter that is able to specific methods and execute all others on the underlying
   * object.
   */
  private class ActiveRecordFilteringMethodInterceptor implements MethodInterceptor {
    private ActiveRecord<?> record;
    private Set<String> disallowedMethods;
    
    public ActiveRecordFilteringMethodInterceptor(ActiveRecord<?> record, Set<String> disallowedMethods) {
      this.record = record;
      this.disallowedMethods = disallowedMethods;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
      String name = method.getName();
      if(disallowedMethods.contains(name)) {
        throw new IllegalAccessException("Access to " + name + " of " + obj.getClass().getName() + " is not allowed.");
      }
      
      // invoke the method on the enclosed record, not underlying instance
      return proxy.invoke(record, args);
    }

  }
  
}
